package main

import (
	"context"
	"fmt"
	"github.com/coreos/etcd/clientv3"
	"log"
	"time"
)

//分布式集群下的optimistic lock，
// 基于etcd的OP实现，
// 基于txn事务 if else then
//基于lease实现锁的ttl(避免节点挂掉后，任务被锁住其他节点无法执行，当然包括日常的keepalive 续租机制)

//1.上锁：创建leaseId，keepalive leaseId，put Key and attached  leaseId
//2.处理业务逻辑
//3.任务完成后或者挂掉后 释放锁（取消keepalive & 马上释放leaseId，进一步删除1中的 key ：leaseId的relation，同时删除key。）
//思考：到底是del了relation还是同时del了leaseId和key value
//答案：看结果是都会del https://pkg.go.dev/github.com/coreos/etcd/clientv3?tab=doc#example-KV-Do

func main() {

	var (
		clientV3Conf clientv3.Config

		cli                *clientv3.Client
		lease              clientv3.Lease
		leaseId            clientv3.LeaseID
		leaseGrantResponse *clientv3.LeaseGrantResponse

		aliveResponsesChan <-chan *clientv3.LeaseKeepAliveResponse
		aliveResponses     *clientv3.LeaseKeepAliveResponse
		ctxWithTimeout     context.Context

		//两种取消context的方法，context是个接口，可以多种实现
		//ctxWithCancel context.Context
		cancelFunc context.CancelFunc

		//leaseRevokeResp *clientv3.LeaseRevokeResponse

		kv  clientv3.KV
		txn clientv3.Txn

		txnResponse *clientv3.TxnResponse

		isGetlock string
	)

	clientV3Conf = clientv3.Config{
		Endpoints:   []string{"10.70.30.73:2379"},
		DialTimeout: 3 * time.Second,
	}
	cli, err := clientv3.New(clientV3Conf)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("hello,etcd  ")

	//1.上锁：创建leaseId，keepalive leaseId，putKye and relate leaseId
	lease = clientv3.NewLease(cli)

	//创建一个10秒的lease,得到一个leaseId
	if leaseGrantResponse, err = lease.Grant(context.TODO(), 10); err != nil {
		fmt.Println(err)
		return
	}
	leaseId = leaseGrantResponse.ID

	//拿到leaseId去put key，要保证在put key返回结果之前keep alive

	fmt.Println("首次签订成功，租约号为:", leaseId)
	//key 与lease Id 关联，用option的WithLease，一般情况是为了程序异常或者机器挂掉，防止锁一直被占用。一般情况是定时续租。分别调用keepAlive 或者keepAliveOnce，后台将会起一个协程

	//5秒后会自动取消的context，之前第一次是10秒，5秒一直续着，应该是一秒一次，所以一共15秒的生命周期，比如租房先签合同一个月，然后按天续约
	ctxWithTimeout, cancelFunc = context.WithTimeout(context.TODO(), 5*time.Second)
	//fmt.Println(ctxWithTimeout)

	//释放锁；会把租约释放，并且把关联的kv也删除
	//可以通过传递cxtWithTimeout，也可以通过传递ctxWithCancel，达到取消context的目的
	//ctxWithCancel, cancelFunc = context.WithCancel(context.TODO())
	//main return 或者机器挂掉，后执行cancelFunc，取消context
	//执行完成后通过执行这个回调函数来释放，可以是witchCancel，也可以是withTimeOut
	defer cancelFunc()
	// 执行完成立即释放掉lease
	defer lease.Revoke(context.TODO(), leaseId)

	//aliveResponsesChan不为nil还是err不为nil？
	if aliveResponsesChan, err = lease.KeepAlive(ctxWithTimeout, leaseId); err != nil {
		fmt.Println(err)
		return
	}

	// 续约成功，会启动一个协程发心跳给etcd，我们需要去消费这个channel
	go func() {
		for {
			select {
			case aliveResponses = <-aliveResponsesChan:
				// 是chan ==nil还是aliveResponses==nil？
				if aliveResponses == nil {
					//sdk与etcd之间心跳异常（因为channel里收到了nil），这时候租约就无效了,比如长时间没有应答，或者主动将context cancel，也会导致租约无效，5秒后会自动取消的context见上文
					fmt.Println("租约无效")
					goto END
				} else {
					fmt.Println("=========keep alive正常，从aliveResponsesChan中收到的租约id为: ", aliveResponses.ID, "ttl为：", aliveResponses.TTL)
				}
			}
		}
	END:
	}()

	//上锁,set key if not exist，else fail

	kv = clientv3.NewKV(cli)

	txn = kv.Txn(context.TODO())

	//如果key1=0，说明没有设置，则设置，否则或者key1的值,该操作为原子性
	txn.If(clientv3.Compare(clientv3.CreateRevision("key1"), "=", 0)).Then(clientv3.OpPut("key1", "value1", clientv3.WithLease(leaseId))).Else(clientv3.OpGet("key1"))

	if txnResponse, err = txn.Commit(); err != nil {
		fmt.Println(err)
	}

	if txnResponse.Succeeded {
		fmt.Println("你抢到了")
		isGetlock = "处理抢到的任务吧！"
	} else {
		fmt.Println("你没抢到，被别人抢到了,key1已经设置为:", string(txnResponse.Responses[0].GetResponseRange().Kvs[0].Value))
		isGetlock = "没抢到任务，下班吧！"
	}
	//任务处理，假设10s
	fmt.Println(isGetlock)
	time.Sleep(10 * time.Second)
	fmt.Println("end")

	//处理完成后上面埋伏的defer会把锁释放掉
}
